const { VIEWSTREAMMAP, VIEWSUBJECTMAP } = require('../lib/const');
const { hashCode } = require('../lib/helper');
const { protectData } = require('../lib/helper');

class Stream {
    constructor(context) {
        this._fn = null;
        this._id = hashCode(this.constructor.toString());
        this._context = context;
    }

    get id() {
        return this._id;
    }

    get context() {
        return this._context;
    }

    oncreated() { }

    onunload() { }

    onpushed() { }

    oncontext({ context, data }) {
        return data;
    }

    next(data) {
        data = protectData(data);
        this._fn(this._id, data);
    }
}

class PullStream extends Stream {
    constructor(context) {
        super(context);
        this._cache = undefined;
        this._pushed = false;
    }

    pull(context) { }

    next(data) {
        data = protectData(data);
        this._cache = data;
        this._pushed = true;
        this._fn(this._id, data);
    }
}

class StreamManager {
    constructor(context) {
        this._context = context;
        this._parent = null;
        this._map = new Map();
    }

    get context() {
        return this._context;
    }

    setParent(parent) {
        this._parent = parent;
    }

    getRoot() {
        let target = this, r = null;
        while (target) {
            if (!target._parent) {
                r = target;
                break;
            } else {
                target = target._parent;
            }
        }
        return r;
    }

    set(streamClass) {
        if (streamClass === Stream || streamClass.prototype instanceof Stream) {
            let root = this.getRoot(), streamKey = hashCode(streamClass.toString());
            if (!root._map.has(streamKey)) {
                let stream = new streamClass(root.context);
                stream.oncreated();
                stream._fn = (streamId, data) => {
                    let target = this.get(streamId), ps = Promise.resolve(), removed = [];
                    if (target) {
                        target.subscriber.forEach(view => {
                            if (!view.isRemoved()) {
                                ps = ps.then(() => {
                                    return this.emitView({ stream: target.stream, view, data });
                                });
                            } else {
                                removed.push(view);
                            }
                        });
                        return ps.then(() => {
                            removed.forEach(a => target.subscriber.delete(a));
                            return target.stream.onpushed();
                        });
                    } else {
                        this.getRoot()._map.delete(streamId);
                        return ps;
                    }
                }
                root._map.set(streamKey, { subscriber: new Set(), stream });
            }
        }
    }

    get(streamId) {
        return this.getRoot()._map.get(streamId);
    }

    emitView({ stream, view, data }) {
        if (view && !view.isRemoved()) {
            let streamId = stream._id, map = view[VIEWSTREAMMAP][streamId];
            if (map) {
                return Promise.resolve().then(() => {
                    return stream.oncontext({ context: view.context, data });
                }).then(data => {
                    if (map.method && !view.isRemoved()) {
                        return view[map.method]({ data, subject: map.subject });
                    }
                }).catch(e => {
                    console.error(e);
                });
            }
        }
        return Promise.resolve();
    }

    subscribe(view) {
        Reflect.ownKeys(view[VIEWSTREAMMAP] || {}).forEach(streamId => {
            let { construct } = view[VIEWSTREAMMAP][streamId];
            !this.get(streamId) && this.set(construct);
        });
        return Reflect.ownKeys(view[VIEWSUBJECTMAP] || {}).reduce((a, subject) => {
            return a.then(() => {
                let { streamId, method } = view[VIEWSUBJECTMAP][subject];
                let info = this.get(streamId);
                if (info && method && !view.isRemoved()) {
                    info.subscriber.add(view);
                    if (info.stream instanceof PullStream) {
                        if (!info.stream._pushed) {
                            return info.stream.pull(view.context);
                        } else {
                            return view[method]({ subject, data: info.stream._cache });
                        }
                    }
                }
                return Promise.resolve();
            });
        }, Promise.resolve());
    }

    unsubscribe(view) {
        Reflect.ownKeys(view[VIEWSUBJECTMAP] || {}).forEach(subject => {
            let { streamId } = view[VIEWSUBJECTMAP][subject], info = this.get(streamId);
            if (info) {
                info.subscriber.delete(view);
                if (info.subscriber.size === 0) {
                    this.getRoot()._map.delete(streamId);
                    info.stream.onunload();
                }
            }
        });
    }
}

/**
 * 分成两种流，一种（PullStream）每次遇到订阅者（view）就会拿数据调订阅方法
 * 普通的流对于订阅者而言错过分发就不会调用订阅方法
 * 流都类似rx中的subject，可以定义自定义方法，在任何时候与其交互
 * 订阅者之间是平权的,获得的数据是一致的，也不存在相互影响
 * 推送的数据，一直推一直有效
 * 全局唯一性，分context数据加工处理，相同数据可以根据不同上下文推处理过的不同的数据
 */

module.exports = { Stream, StreamManager, PullStream };